Skip to content

Offer Google Antigravity as a Bot, and let a harness need one vendor's key - #808

Merged
guidovizoso merged 11 commits into
mainfrom
guido/antigravity-agent
Oct 19, 2026
Merged

guidovizoso merged 11 commits into
mainfrom
guido/antigravity-agent

Conversation

@guidovizoso

Copy link
Copy Markdown
Collaborator

Adds Google Antigravity to the harness catalogue and ships it as a Bot, which turned out to need one
change to how a harness states what it wants and one to how a Bot reads the provider spec.

A harness can need one vendor's key

Every other row in the catalogue takes any-provider: the Bot reaches its model through LiteLLM or
an equivalent, so moving a deployment to Anthropic is a setting rather than a break. Antigravity is
not like that. Its SDK drives a bundled Go localharness that wants a Gemini key wherever its base
url points, so credential gained a google variant and the picker asks for the right key instead
of offering the row to a deployment holding only an OpenAI one.

shared/model-providers.json gained the matching google provider row, and pinned_provider in
shared/model_providers.py is what makes BOT_PROVIDER=openai unable to drag this one Bot along —
it would otherwise start, accept work, and fail every call.

The Bot

agent-antigravity/ is a small AG-UI server like its ten Python siblings: the same token gate on
every path but /health, the same venv-per-Bot step in CI, the same row in
.github/published-images.json.

Two things are not like the siblings, both measured rather than assumed:

  • python:3.12-slim, not alpine. The SDK wheel bundles a Go binary built against
    manylinux_2_17, and musl cannot load it.
  • The base url is the API root, with no version path. The harness appends
    /v1beta/models/<model>:streamGenerateContent itself. A url carrying the version gets it twice and
    404s, which the harness reports as the unplaceable agent executor error: Error 0, Message: Not found — naming neither the url nor the status. Found with a logging proxy.

That second one came out of a real gap: the Bot ignored the base_url_variable its own provider row
declares, and read GOOGLE_API_KEY by hand. Both names now come from the row, so a deployment can
point this Bot at a gateway, a proxy holding the real key, or a mock.

Also here

app/src/lib/plugins/tool-name.ts — a Composio tool call read "Gmail fetch emails" with a muted
composio-gmail beside it. The broker is how the credential is held, which belongs on the Bot's
access page rather than in the transcript; this is the argument the file already makes for
relabelling research calls. Independent of the rest of this branch and reviewable on its own.

Checks

bun test 7054 pass / 0 fail. agent-antigravity pytest 14 pass. Format, lint, typecheck and build
clean; cargo test harness 43 pass; the Dockerfile builds; published-images.json matches the
*/Dockerfile set exactly. actionlint is not installed on the machine this ran on, so the new CI
step was reviewed by hand against its ten siblings rather than linted.

Known gaps, not addressed here

Every harness so far either takes whatever provider the deployment chose or
brings its own address, so `credential` had three values and `any-provider` was
the default answer. Antigravity is the first that is neither: its bundled
harness requires a Gemini key wherever its base url points, so offering it as
"any provider you already have" would install it against an OpenAI-only
deployment and produce a Bot that starts, accepts work, and fails every call.

That is the failure `webCreate` already exists to prevent on the web side, so
this is the same decision made one level down: `google` is its own credential,
the desktop picker says "Needs a Gemini key" on the row, and the test that every
row's credential is one the type names keeps the two lists from drifting.

Nothing changes for the rows that were already there.
AG-UI publishes an Antigravity integration, so this is the thirteenth harness
and mostly follows the twelve before it: a small FastAPI service, the shared
token gate, an open /health for Compose to poll, a row in the catalogue, and
the vendored mark from the same MIT set as the rest.

Two things are not like the others.

The provider is PINNED. Every other Bot reads `BOT_PROVIDER`, because they all
reach their model through LiteLLM or an equivalent and moving them is a setting.
Antigravity's SDK drives a bundled Go harness that wants a Gemini key wherever
its base url points, so a deployment that moved its other Bots to OpenAI must
not drag this one along — `bot_settings(..., pinned_provider="google")` is what
stops it. The model is still the deployment's to choose: `BOT_MODEL` names which
Gemini, and the pin only holds the provider.

The image is glibc, not musl, and the Dockerfile says why: the SDK ships a
platform wheel bundling a Go `localharness` built against manylinux_2_17, and
alpine cannot load it. Proved rather than assumed — the linux/amd64 image was
built and run, /health answered, a POST without the server token was refused
401, and the same POST with it reached the AG-UI endpoint.

`.github/published-images.json` gains the image, which is the file CI checks
against the Dockerfiles in the tree and the release derives its build matrix
from, so one entry wires both.

Worth knowing for whoever reviews the security of this: that Go harness does
real file and shell work. In this image it has no volumes and no host mount, so
its reach is the container — but it sits outside agent-computer's barriers,
authorisation and handoff entirely, which is where every other Bot's computer
work goes.
CI gives each Python Bot an environment named after it — `.venv-adk`,
`.venv-antigravity` — and `.gitignore` only knew the bare `.venv` and `venv`
spellings. Running those same commands locally, which is the obvious way to
check a Python Bot before pushing, left hundreds of megabytes of untracked
platform binaries in the tree.

Biome does not read `.gitignore`, so it needs telling separately: linting a
freshly made environment reported fifteen errors inside urllib3 and failed
`bun run lint` for reasons that had nothing to do with the change in hand.
The `google` row in `shared/model-providers.json` declares a `base_url_variable`, and this Bot
ignored it: it read `GOOGLE_API_KEY` by hand and had no way to reach a Gemini-compatible endpoint
that was not Google's. Both variable names now come from the row, so renaming either is one edit to
the file every language reads.

A base url is spelled as `endpoint=GeminiAPIEndpoint(...)`. `AntigravityAgent(base_url=...)` is a
different and mutually exclusive path, meant for unauthenticated local servers, and it drops the
key — so the test asserts the key came along rather than only grepping the source.

That url is the API ROOT, with no version path. The harness appends
`/v1beta/models/<model>:streamGenerateContent` itself, and a url carrying the version gets it twice
and 404s, which the harness reports as the unplaceable `agent executor error: Error 0, Message: Not
found`. Measured against a logging proxy, because that message names neither the url nor the status.
A Composio tool call read "Gmail fetch emails" with a muted "composio-gmail" beside it. The broker
is how the credential is held, which is an administrator's fact on the Bot's access page, not
something the person who asked about their email needs on screen — the same argument this file
already makes for relabelling research calls.

Two things kept it there. The prefix was never stripped, and the rule that drops a server the label
already names skips the label's first word, so a verb cannot be mistaken for the server. Composio
publishes `<APP>_<VERB>_<NOUN>`, so the app always leads and was always the word being skipped.

The app is still shown when the action does not name it — `FETCH_EMAILS` keeps `gmail` as its
detail. Only the broker is dropped, never what was acted upon.
`gemini-2.5-flash` is retired: Google answers a request for it with 404 "no longer available to new
users", so this Bot as merged would have failed every run on a fresh deployment. Measured against a
real key, not read off a changelog — `gemini-3.6-flash` answers through the harness.

The provider row's own `default_model` is stale in the same way, but it is not this Bot's: the pin
means `bot_settings` reads the `bots` entry, never the provider fallback. Left alone here rather
than widened into a change to every Bot a deployment might move to Google.

The `bots` block is back to one object per line. Adding an entry had reformatted all thirteen,
which made a one-line change read as fifty-six; biome accepts either, so the churn bought nothing.

The override test now names a model the file does not, because an override asserting the row's own
value passes whether or not the environment is read.
The managed Bot and any harness picked during setup refuse every call that does not carry the
deployment's token, and nobody types that token anywhere: the run path attaches it by recognising
the address. The connection test did not, so testing the Bot in the box answered "The agent refused
this request. If it needs a key, add it as a header." — a refusal reported for a Bot that works, and
advice that would have broken it.

That is the worst case for a wrong verdict. The address was filled in by this deployment rather than
typed by the person reading the result, so they cannot tell this from a real refusal.

The comparison moves to `managed-endpoint.ts` and both paths call it. They had already drifted once
in the other direction — matching a single endpoint left the picked harness registered, routed to,
and answering 401 to everything — so the rule is written down once rather than twice.

An address this deployment does not run is still knocked on bare. The token's whole value is that it
is ours, and the tests pin that alongside the trailing-slash case and a deployment with no managed
Bot at all.
With the token attached the test reached the agent, got its 200, and still failed: "The agent
started answering and the connection broke." Nothing broke. The opening read stopped at an eight
kilobyte cap or the end of the stream, and an agentic harness supplies neither — Antigravity opens
with `RUN_STARTED`, ninety bytes, then goes away to call a model and run tools while holding the
stream open. The read waited out the whole run and was cut off by the request timeout.

It now stops at the first recognised AG-UI event, which is the entire question the test asks.

Events rather than bytes, deliberately: a stream of SSE comments or heartbeats would satisfy a
"something arrived" test while being no evidence of an agent at all. There is a case for that.
The tile reads `credential === "google"` to say "Needs a Gemini key", but the type it reads from
still listed three variants, so the comparison could never be true and `tsc` refused it. The desktop
app is outside the workspace and reads the catalogue over the Tauri boundary as JSON, so it cannot
import the union from shared/harnesses.ts — it keeps its own copy, and adding a variant means adding
it in both places. Said so above the type, since nothing else will.

`cargo fmt` on the catalogue parse, which rustfmt wanted joined onto one line.

Both were missed locally for the same reason: `bun run typecheck` covers the workspace, and desktop
is not in it.
Every CrewAI run started failing with `APIConnectionError: cannot import name 'CacheableResult'
from 'mcp.types'` — a model call reporting a missing import, which reads as a network fault and is
not one. Nothing in this repository changed: `agent-crewai/requirements.txt` pinned nothing, and
litellm 1.106.0 began importing a symbol that exists only in mcp 2.x while ag-ui-crewai 1.0.0
requires mcp<2. The two cannot both be satisfied.

Held litellm rather than raising mcp, because mcp>=2 is reachable only by letting the resolver walk
ag-ui-crewai back to 0.3.1 and crewai to 1.6.1 — a silent downgrade of the harness under test.
1.105.1 does not reference the symbol anywhere and leaves every other version where CI already had
it: ag-ui-crewai 1.0.0, crewai 1.15.28, mcp 1.28.1. Resolved and imported locally to check.

Only this harness is affected. The other five that name litellm resolve without mcp at all, so the
import is never reached; ADK was checked directly because it runs next in the same job.

Unrelated to the rest of this branch, and in it only because the job stops at the first harness that
fails — which left the Antigravity step skipped rather than run.
The CrewAI pin fixed CrewAI and the job walked on to LlamaIndex, which failed the same way with a
different face: `APIConnectionError: OpenAIException - No module named 'mcp'`. litellm 1.106.0
reaches for mcp during a model call and reports whatever it finds as an OpenAIException, so the
error arrives as a connection fault rather than the missing import it is.

Both ends of that break: an older mcp pinned by another dependency gives `cannot import name
'CacheableResult' from 'mcp.types'`, and no mcp at all gives `No module named 'mcp'`. I had checked
ADK, seen mcp absent, and concluded it was safe — absent is the second failure, not the absence of
one, so the remaining five are pinned together rather than one per CI round trip.

Verified by resolving and running the suite that actually failed: agent-llamaindex on litellm
1.105.1, 10 passed.
@guidovizoso
guidovizoso merged commit eb14e32 into main Oct 19, 2026
26 checks passed
@guidovizoso
guidovizoso deleted the guido/antigravity-agent branch October 19, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant